Skip to content

Show the nostr pubkey, and explain why a swap server is undiscoverable - #5

Merged
nothing-stops-this-train merged 1 commit into
mainfrom
feat/nostr-pubkey-and-discovery-diagnostics
Jul 28, 2026
Merged

Show the nostr pubkey, and explain why a swap server is undiscoverable#5
nothing-stops-this-train merged 1 commit into
mainfrom
feat/nostr-pubkey-and-discovery-diagnostics

Conversation

@nothing-stops-this-train

Copy link
Copy Markdown
Owner

Splits the Swap Server tab's output pane into Status and Diagnostics
sub-tabs. The Settings box stays outside them, reachable from both.

Status: the nostr identity

Shows the server's npub, the identicon takers see in their provider list, the
hex pubkey in a tooltip, and a Copy button. The key is seed-derived
(lnworker.nostr_keypair), so it renders whether or not the server is running,
and it is exactly the string a taker pins as SWAPSERVER_NPUB.

Diagnostics: why nobody can see the server

A swap server can be running, connected, and grinding a valid proof of work and
still be completely invisible, because every rejection on the taker side is
silent — NostrTransport._get_pairs_loop drops a candidate offer with at most a
debug log line, and publish_offer returns early (one warning) when there is
no liquidity to advertise.

  • The tab no longer claims to be "announcing" when the path is blocked.
    announce_state() classifies it: no relays / stopped / waiting for the wallet
    password (run_nostr_server blocks on this) / waiting for proof of work / no
    liquidity. The liquidity gate mirrors publish_offer's exactly, including
    that max_forward is capped by both lightning inbound and the on-chain
    balance, and that both maxima are rounded down to two leading digits — so
    the 20,000 sat minimum is easy to miss.

  • The three values a taker's filter must match byte-for-byte are shown
    verbatim: net:<NET_NAME> (signet and mutinynet are distinct upstream),
    the electrum-swapserver-<version> d-tag, and the event kind.

  • Proof-of-work asymmetry is called out. SWAPSERVER_POW_TARGET means
    "bits to grind" on the server but "bits I demand" on the taker, so lowering it
    to save CPU makes the server invisible to every taker still on Electrum's
    default of 30. The tab warns whenever the announcement carries fewer bits.

  • "Check discoverability" queries each configured relay the way another
    wallet would, with a throwaway key, and names the first rule that rejects us —
    per relay. It runs two queries: one by author (is our announcement stored here
    at all?) and one with the taker's verbatim filter (does it survive it?). That
    split is what separates a wrong tag from a missing event from being crowded
    out of a busy relay's 10-result page.

No change to upstream's announcement cadence: the NIP-40 expiry and the
10-minute republish interval are left alone. A stale offer is instead made
visible through the check.

Tests

160 tests, green with and without PyQt6.

  • test_nostr_check.py — the rule engine, pinned to the order of checks
    _get_pairs_loop performs, so the field we blame is the one that rejects us
    first upstream.
  • test_diagnostics.py — the announcement state machine, the liquidity gate
    boundary, and the npub round-tripping back to the x-only pubkey.
  • test_discovery_e2e.py + fake_relay.py — the check driven against a real
    NIP-01 relay hosted in-process over aiohttp WebSocket. Offers are published
    with electrum_aionostr exactly as the server does and signatures are verified
    on receipt; low PoW, wrong net, wrong version, crowded out, unreachable and
    rejected-write are each asserted to be named correctly, not merely detected.
  • test_qt_tab.py — builds the real SwapServerTab on Qt's offscreen platform
    and drives refresh(). Skips without PyQt6; CI now installs it so these run
    there (this is why the workflow file changes).

Caveat

This makes the failure legible, it does not fix it. If the cause is missing
liquidity or a network/version mismatch, the check will name it, but resolving
it is a funding or config action on the operator's side.

🤖 Generated with Claude Code

https://claude.ai/code/session_013zETFC6MhnsCuxvQDGxqV5

Adds a Status/Diagnostics sub-tab pair to the Swap Server tab's output pane
(the Settings box stays outside them, reachable from both).

Status now shows the server's nostr identity: the npub, the hex pubkey in a
tooltip, the identicon takers see in their provider list, and a Copy button.
The key is seed-derived (lnworker.nostr_keypair), so it renders whether or not
the server is running, and it is exactly the string a taker pins as
SWAPSERVER_NPUB.

Diagnostics addresses the second half: a swap server can be running, connected
and grinding a valid proof of work and still be completely invisible, because
every rejection on the taker side is silent. NostrTransport._get_pairs_loop
drops a candidate offer with at most a debug log line, and publish_offer
returns early -- with one warning -- when there is no liquidity to advertise.

  * The tab no longer claims to be "announcing" when the announcement path is
    blocked. announce_state() classifies it: no relays, stopped, waiting for
    the wallet password (run_nostr_server blocks on this), waiting for the
    proof of work, or no liquidity. The liquidity gate mirrors publish_offer's
    exactly, including that max_forward is capped by BOTH lightning inbound and
    the on-chain balance and that both maxima are rounded down to two leading
    digits, so the 20,000 sat minimum is easy to miss.

  * The three values a taker's filter must match byte-for-byte are shown
    verbatim: net:<NET_NAME> (signet and mutinynet are distinct), the
    electrum-swapserver-<version> d-tag, and the event kind.

  * SWAPSERVER_POW_TARGET means "bits to grind" on the server but "bits I
    demand" on the taker, so lowering it to save CPU makes the server invisible
    to every taker still on Electrum's default of 30. The tab warns whenever
    the announcement carries fewer bits than that default.

  * "Check discoverability" queries each configured relay the way another
    wallet would, with a throwaway key, and names the first rule that rejects
    us -- per relay. It runs two queries: one by author (is our announcement
    stored here at all?) and one with the taker's verbatim filter (does it
    survive it?). That split is what separates a wrong tag from a missing event
    from being crowded out of a busy relay's 10-result page.

No change to upstream's announcement cadence: the NIP-40 expiry and the
10-minute republish interval are left alone, and a stale offer is instead made
visible through the check.

Tests:
  * test_nostr_check.py  - the rule engine, pinned to the order of checks
    _get_pairs_loop performs so the field we blame is the one that rejects us
    first upstream.
  * test_diagnostics.py  - the state machine, the liquidity gate boundary, and
    the npub round-tripping back to the x-only pubkey.
  * test_discovery_e2e.py + fake_relay.py - the check driven against a real
    NIP-01 relay hosted in-process over aiohttp WebSocket. Offers are published
    with electrum_aionostr exactly as the server does and signatures are
    verified on receipt; low PoW, wrong net, wrong version, crowded out,
    unreachable and rejected-write are each asserted to be named correctly.
  * test_qt_tab.py - builds the real SwapServerTab on Qt's offscreen platform
    and drives refresh(). Skips without PyQt6; CI now installs it so these run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zETFC6MhnsCuxvQDGxqV5
@nothing-stops-this-train
nothing-stops-this-train merged commit 971b1ed into main Jul 28, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant